-
Notifications
You must be signed in to change notification settings - Fork 4
feat: marketplace #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: marketplace #345
Conversation
WalkthroughAdds a complete “marketplace” platform: full client app (React/Vite/Tailwind), shared schema, server (Express/Passport/Drizzle/Neon), object storage (GCS) with ACLs, admin dashboard with CRUD and uploads, public pages, extensive UI component library, React Query setup, auth context, build tooling, and configs. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Admin as Admin User
participant UI as Client (Admin Dashboard)
participant API as Server (Express)
participant DB as DB (Drizzle/Neon)
Admin->>UI: Open /admin
UI->>API: GET /api/user
API-->>UI: { user, isAdmin }
alt not admin
UI->>UI: Redirect to /admin/auth
Admin->>UI: Submit login (email, password)
UI->>API: POST /api/login
API->>DB: Verify user (scrypt)
DB-->>API: User
API-->>UI: 200 OK (session)
UI->>API: GET /api/user
API-->>UI: { user, isAdmin: true }
end
UI->>API: GET /api/admin/apps
API->>DB: Select apps
DB-->>API: Apps
API-->>UI: Apps list
sequenceDiagram
autonumber
actor Admin as Admin User
participant UI as Client (Admin Dashboard)
participant API as Server
participant OSS as ObjectStorageService
participant GCS as GCS
Admin->>UI: Click "Upload Logo"
UI->>API: POST /api/objects/upload
API->>OSS: getObjectEntityUploadURL()
OSS->>GCS: Sign URL (PUT)
GCS-->>OSS: Signed URL
OSS-->>API: URL
API-->>UI: { url }
UI->>GCS: PUT file to signed URL
UI->>API: PUT /api/objects/finalize { fileURL }
API->>OSS: trySetObjectEntityAclPolicy(OWNER+PUBLIC_READ)
OSS->>GCS: Set metadata ACL
OSS-->>API: objectPath
API-->>UI: { objectPath }
UI->>API: POST /api/admin/apps (logoUrl=objectPath)
API->>DB: Insert app
DB-->>API: App
API-->>UI: 201 Created
sequenceDiagram
autonumber
actor User as Visitor
participant UI as Client (App Detail)
participant API as Server
participant DB as DB
User->>UI: Open /app/:id
UI->>API: GET /api/apps/:id
API->>DB: Fetch app
DB-->>API: App
API-->>UI: App
UI->>API: GET /api/apps/:id/reviews
API->>DB: Fetch reviews
DB-->>API: Reviews
API-->>UI: Reviews
User->>UI: Submit review
UI->>API: POST /api/apps/:id/reviews
API->>DB: Insert review + update average
DB-->>API: OK
API-->>UI: 201 Created
UI->>API: GET /api/apps/:id/reviews (invalidate)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~150 minutes Suggested labels
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 65
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove maximum-scale=1
for accessibility (disables zoom).
This blocks pinch-zoom and can violate WCAG expectations.
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
🤖 Prompt for AI Agents
In platforms/marketplace/client/index.html around line 5, the meta viewport tag
includes maximum-scale=1 which disables pinch-zoom and harms accessibility;
remove maximum-scale=1 (and any user-scalable=no) from the content attribute so
the viewport allows zooming, leaving something like width=device-width,
initial-scale=1.0 (and optionally add a comment explaining why zoom must remain
enabled).
@@ -0,0 +1,67 @@ | |||
import { useState } from "react"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add useEffect for proper Uppy lifecycle management.
You create an Uppy instance but never close it on unmount, leaking listeners/state across mounts.
-import { useState } from "react";
+import { useState, useEffect } from "react";
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
import { useState } from "react"; | |
import { useState, useEffect } from "react"; |
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ObjectUploader.tsx around line 1,
the component creates an Uppy instance but never tears it down, leaking
listeners and state across mounts; wrap Uppy creation inside a useEffect (or
create it once with useRef and initialize in useEffect) and in the effect's
cleanup call uppy.close() (and null out any state/ref if used), ensure the
effect has an appropriate dependency array so an Uppy instance is only created
once per component lifecycle, and update any setState usage to happen inside the
effect after creating Uppy.
const [uppy] = useState(() => | ||
new Uppy({ | ||
restrictions: { | ||
maxNumberOfFiles, | ||
maxFileSize, | ||
allowedFileTypes: ['image/*'], | ||
}, | ||
autoProceed: false, | ||
}) | ||
.use(AwsS3, { | ||
shouldUseMultipart: false, | ||
getUploadParameters: onGetUploadParameters, | ||
}) | ||
.on("complete", (result) => { | ||
onComplete?.(result); | ||
setShowModal(false); | ||
}) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Close Uppy on unmount and surface upload errors.
Ensure cleanup and basic error propagation; prevents memory leaks and silent failures.
const [uppy] = useState(() =>
new Uppy({
restrictions: {
maxNumberOfFiles,
maxFileSize,
allowedFileTypes: ['image/*'],
},
autoProceed: false,
})
.use(AwsS3, {
- shouldUseMultipart: false,
getUploadParameters: onGetUploadParameters,
})
.on("complete", (result) => {
onComplete?.(result);
setShowModal(false);
})
+ .on("upload-error", (_file, error) => {
+ console.error("Upload failed:", error);
+ })
);
+
+ useEffect(() => {
+ return () => {
+ uppy.close();
+ };
+ }, [uppy]);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const [uppy] = useState(() => | |
new Uppy({ | |
restrictions: { | |
maxNumberOfFiles, | |
maxFileSize, | |
allowedFileTypes: ['image/*'], | |
}, | |
autoProceed: false, | |
}) | |
.use(AwsS3, { | |
shouldUseMultipart: false, | |
getUploadParameters: onGetUploadParameters, | |
}) | |
.on("complete", (result) => { | |
onComplete?.(result); | |
setShowModal(false); | |
}) | |
); | |
const [uppy] = useState(() => | |
new Uppy({ | |
restrictions: { | |
maxNumberOfFiles, | |
maxFileSize, | |
allowedFileTypes: ['image/*'], | |
}, | |
autoProceed: false, | |
}) | |
.use(AwsS3, { | |
getUploadParameters: onGetUploadParameters, | |
}) | |
.on("complete", (result) => { | |
onComplete?.(result); | |
setShowModal(false); | |
}) | |
.on("upload-error", (_file, error) => { | |
console.error("Upload failed:", error); | |
}) | |
); | |
useEffect(() => { | |
return () => { | |
uppy.close(); | |
}; | |
}, [uppy]); |
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ObjectUploader.tsx around lines
34 to 51, the Uppy instance is created but not cleaned up and upload errors
aren't surfaced; add a useEffect that registers an "error" event handler on uppy
to call an onError callback (or at least console.error) and returns a cleanup
function that removes the handler and closes/destroys the uppy instance
(uppy.close() or uppy.destroy()) on unmount to prevent memory leaks and surface
upload failures to the parent.
function Badge({ className, variant, ...props }: BadgeProps) { | ||
return ( | ||
<div className={cn(badgeVariants({ variant }), className)} {...props} /> | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use forwardRef for ref support and interop with parent components
Forwarding refs is standard for UI primitives (focus, measurements, animations). Add displayName for DevTools.
Apply:
-function Badge({ className, variant, ...props }: BadgeProps) {
- return (
- <div className={cn(badgeVariants({ variant }), className)} {...props} />
- )
-}
+const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
+ ({ className, variant, ...props }, ref) => (
+ <div ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} />
+ )
+)
+Badge.displayName = "Badge"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
function Badge({ className, variant, ...props }: BadgeProps) { | |
return ( | |
<div className={cn(badgeVariants({ variant }), className)} {...props} /> | |
) | |
} | |
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>( | |
({ className, variant, ...props }, ref) => ( | |
<div ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} /> | |
) | |
) | |
Badge.displayName = "Badge" |
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/badge.tsx around lines 30-34,
the Badge component does not forward refs or set a displayName; update it to use
React.forwardRef so it accepts a ref parameter and forwards it to the root div
(e.g., forwardRef<HTMLDivElement, BadgeProps>((props, ref) => { ... })) and pass
ref into the div along with existing props, then set Badge.displayName = "Badge"
for DevTools; ensure the component export remains the same and adjust any type
imports to include ForwardRefRenderFunction/ForwardedRef types as needed.
const Comp = asChild ? Slot : "button" | ||
return ( | ||
<Comp | ||
className={cn(buttonVariants({ variant, size, className }))} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix className merge and default button type (prevents lost styles and accidental form submits).
- Passing
className
insidebuttonVariants({ ... })
is ignored by CVA, so consumer styles never apply. - Also default the native button
type
to "button" when not usingasChild
.
-const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+ ({ className, variant, size, asChild = false, type, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
- className={cn(buttonVariants({ variant, size, className }))}
+ className={cn(buttonVariants({ variant, size }), className)}
ref={ref}
+ {...(!asChild && { type: type ?? "button" })}
{...props}
/>
)
}
)
Also applies to: 42-53
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/button.tsx around lines 42 to
53 (specifically line 47), the component currently passes className into
buttonVariants (CVA) which ignores it and causes consumer styles to be lost;
also the native button type isn't defaulted causing accidental form submits.
Remove className from the object passed to buttonVariants, call buttonVariants
with only variant/size/etc and then merge the result with the incoming className
using cn(...), and set the rendered native button's default prop type="button"
when asChild is false and no explicit type prop was provided (preserve provided
type when present). Ensure the final className passed to the element is the
merged value and that asChild handling remains unchanged.
async updateAppRating(appId: string): Promise<void> { | ||
const result = await db | ||
.select({ | ||
avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`, | ||
count: sql<number>`COUNT(*)`, | ||
}) | ||
.from(reviews) | ||
.where(eq(reviews.appId, appId)); | ||
|
||
const { avgRating, count } = result[0]; | ||
|
||
await db | ||
.update(apps) | ||
.set({ | ||
averageRating: avgRating || "0.00", | ||
totalReviews: count || 0, | ||
updatedAt: new Date(), | ||
}) | ||
.where(eq(apps.id, appId)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Cast COUNT(*) to int to avoid bigint-as-string pitfalls
Postgres returns COUNT(*)
as int8
, which pg
often surfaces as string. Cast to int
to ensure totalReviews
receives a number.
- .select({
- avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`,
- count: sql<number>`COUNT(*)`,
- })
+ .select({
+ avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`,
+ count: sql<number>`COUNT(*)::int`,
+ })
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
async updateAppRating(appId: string): Promise<void> { | |
const result = await db | |
.select({ | |
avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`, | |
count: sql<number>`COUNT(*)`, | |
}) | |
.from(reviews) | |
.where(eq(reviews.appId, appId)); | |
const { avgRating, count } = result[0]; | |
await db | |
.update(apps) | |
.set({ | |
averageRating: avgRating || "0.00", | |
totalReviews: count || 0, | |
updatedAt: new Date(), | |
}) | |
.where(eq(apps.id, appId)); | |
} | |
async updateAppRating(appId: string): Promise<void> { | |
const result = await db | |
.select({ | |
avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`, | |
count: sql<number>`COUNT(*)::int`, | |
}) | |
.from(reviews) | |
.where(eq(reviews.appId, appId)); | |
const { avgRating, count } = result[0]; | |
await db | |
.update(apps) | |
.set({ | |
averageRating: avgRating || "0.00", | |
totalReviews: count || 0, | |
updatedAt: new Date(), | |
}) | |
.where(eq(apps.id, appId)); | |
} |
🤖 Prompt for AI Agents
In platforms/marketplace/server/storage.ts around lines 117 to 136, the SQL
selection uses COUNT(*) which Postgres returns as int8 (bigint) and may be
surfaced as a string; change the COUNT expression to cast to int (e.g.,
CAST(COUNT(*) AS int) or COUNT(*)::int) and update the sql type annotation to
sql<number> so totalReviews is returned as a number, ensuring the subsequent
update uses a numeric count rather than a string.
@@ -0,0 +1,96 @@ | |||
import type { Config } from "tailwindcss"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ESM/CJS mismatch: replace require(...)
with ESM imports
This file is ESM (export default … satisfies Config
). Using require(...)
will fail (“require is not defined”). Import the plugins and reference them directly.
-import type { Config } from "tailwindcss";
+import type { Config } from "tailwindcss";
+import animate from "tailwindcss-animate";
+import typography from "@tailwindcss/typography";
@@
- plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
+ plugins: [animate, typography],
Also applies to: 95-96
🤖 Prompt for AI Agents
In platforms/marketplace/tailwind.config.ts around lines 1 and 95-96, the file
is ESM but currently uses require(...) for plugins which will fail; replace
those require(...) calls with top-level ESM import statements (import pluginName
from 'plugin-package') and then reference the imported identifiers directly in
the config export, removing any require usage and ensuring the imports are added
to the top of the file.
"incremental": true, | ||
"tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo", | ||
"noEmit": true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
tsBuildInfo path under node_modules is brittle; write to a repo-owned file instead
TypeScript will write the build info even with noEmit
. Writing into node_modules
can fail in CI/PNPM or read-only FS.
"compilerOptions": {
"incremental": true,
- "tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo",
+ "tsBuildInfoFile": "./.tsbuildinfo",
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
"incremental": true, | |
"tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo", | |
"noEmit": true, | |
"incremental": true, | |
"tsBuildInfoFile": "./.tsbuildinfo", | |
"noEmit": true, |
🤖 Prompt for AI Agents
In platforms/marketplace/tsconfig.json around lines 5–7 the tsBuildInfoFile is
pointed at "./node_modules/typescript/tsbuildinfo", which is brittle in CI/PNPM
or read-only filesystems; change tsBuildInfoFile to a repo-owned path (for
example "./.tsbuildinfo" or "./.cache/tsbuildinfo" or "./build/tsbuildinfo")
instead of anything under node_modules, and ensure the chosen directory exists
or is created by the build, adding the tsbuildinfo filename to .gitignore if
desired.
"moduleResolution": "bundler", | ||
"baseUrl": ".", | ||
"types": ["node", "vite/client"], | ||
"paths": { | ||
"@/*": ["./client/src/*"], | ||
"@shared/*": ["./shared/*"] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify path alias support in Vite config
Aliases work in TS, but bundler also needs resolve aliases or vite-tsconfig-paths
. Ensure the Vite config enables this.
To check:
🏁 Script executed:
#!/bin/bash
# Look for tsconfig-paths plugin or manual resolve.alias
rg -nP -C2 'tsconfigPaths|resolve\s*:\s*\{[^}]*alias' platforms/marketplace
Length of output: 77
🏁 Script executed:
#!/bin/bash
# Locate Vite config files in platforms/marketplace
find platforms/marketplace -maxdepth 3 -type f -iname 'vite.config.*'
# For each found config, print any resolve.alias block
for cfg in $(find platforms/marketplace -maxdepth 3 -type f -iname 'vite.config.*'); do
echo "=== $cfg ==="
rg -nP -C3 'resolve\s*:\s*\{[^}]*alias' "$cfg" || echo "No resolve.alias in $cfg"
done
Length of output: 568
Configure path aliases in Vite config
The marketplace’s vite.config.ts
currently has no resolve.alias
entries or the vite-tsconfig-paths
plugin enabled, so your TypeScript aliases (@/*
, @shared/*
) won’t be recognized at build time. In platforms/marketplace/vite.config.ts
, either:
- Install and register
vite-tsconfig-paths
:import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ plugins: [tsconfigPaths(), /* … */], // … });
- Or add a matching
resolve.alias
block:import { resolve } from 'path'; export default defineConfig({ resolve: { alias: [ { find: '@', replacement: resolve(__dirname, 'client/src') }, { find: '@shared', replacement: resolve(__dirname, 'shared') }, ], }, // … });
This ensures your TS path aliases work during bundling.
🤖 Prompt for AI Agents
In platforms/marketplace/vite.config.ts (configure to match tsconfig.json paths
referenced at platforms/marketplace/tsconfig.json lines 15-21), the TypeScript
path aliases "@/*" and "@shared/*" are not recognized by Vite; update
vite.config.ts to either register the vite-tsconfig-paths plugin (install it and
add tsconfigPaths() to the plugins array) or add a resolve.alias section that
maps "@" to client/src and "@shared" to shared using absolute paths resolved
from __dirname so the build respects your tsconfig paths.
export default defineConfig({ | ||
plugins: [ | ||
react(), | ||
runtimeErrorOverlay(), | ||
...(process.env.NODE_ENV !== "production" && | ||
process.env.REPL_ID !== undefined | ||
? [ | ||
await import("@replit/vite-plugin-cartographer").then((m) => | ||
m.cartographer(), | ||
), | ||
] | ||
: []), | ||
], | ||
resolve: { | ||
alias: { | ||
"@": path.resolve(import.meta.dirname, "client", "src"), | ||
"@shared": path.resolve(import.meta.dirname, "shared"), | ||
"@assets": path.resolve(import.meta.dirname, "attached_assets"), | ||
}, | ||
}, | ||
root: path.resolve(import.meta.dirname, "client"), | ||
build: { | ||
outDir: path.resolve(import.meta.dirname, "dist/public"), | ||
emptyOutDir: true, | ||
}, | ||
server: { | ||
fs: { | ||
strict: true, | ||
deny: ["**/.*"], | ||
}, | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import.meta.dirname
is not standard; fix path resolution and dynamic plugin import
Use fileURLToPath
to derive the directory, and move the conditional dynamic import into an async defineConfig
factory. Keeps ESM-safe and works across Node environments.
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-import path from "path";
-import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
-
-export default defineConfig({
- plugins: [
- react(),
- runtimeErrorOverlay(),
- ...(process.env.NODE_ENV !== "production" &&
- process.env.REPL_ID !== undefined
- ? [
- await import("@replit/vite-plugin-cartographer").then((m) =>
- m.cartographer(),
- ),
- ]
- : []),
- ],
- resolve: {
- alias: {
- "@": path.resolve(import.meta.dirname, "client", "src"),
- "@shared": path.resolve(import.meta.dirname, "shared"),
- "@assets": path.resolve(import.meta.dirname, "attached_assets"),
- },
- },
- root: path.resolve(import.meta.dirname, "client"),
- build: {
- outDir: path.resolve(import.meta.dirname, "dist/public"),
- emptyOutDir: true,
- },
- server: {
- fs: {
- strict: true,
- deny: ["**/.*"],
- },
- },
-});
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import path from "path";
+import { fileURLToPath } from "url";
+import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
+
+export default defineConfig(async () => {
+ const rootDir = path.dirname(fileURLToPath(import.meta.url));
+ const plugins = [react(), runtimeErrorOverlay()];
+
+ if (process.env.NODE_ENV !== "production" && process.env.REPL_ID) {
+ const { cartographer } = await import("@replit/vite-plugin-cartographer");
+ plugins.push(cartographer());
+ }
+
+ return {
+ plugins,
+ resolve: {
+ alias: {
+ "@": path.resolve(rootDir, "client", "src"),
+ "@shared": path.resolve(rootDir, "shared"),
+ "@assets": path.resolve(rootDir, "attached_assets"),
+ },
+ },
+ root: path.resolve(rootDir, "client"),
+ build: {
+ outDir: path.resolve(rootDir, "dist/public"),
+ emptyOutDir: true,
+ },
+ server: {
+ fs: {
+ strict: true,
+ deny: ["**/.*"],
+ },
+ },
+ };
+});
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
export default defineConfig({ | |
plugins: [ | |
react(), | |
runtimeErrorOverlay(), | |
...(process.env.NODE_ENV !== "production" && | |
process.env.REPL_ID !== undefined | |
? [ | |
await import("@replit/vite-plugin-cartographer").then((m) => | |
m.cartographer(), | |
), | |
] | |
: []), | |
], | |
resolve: { | |
alias: { | |
"@": path.resolve(import.meta.dirname, "client", "src"), | |
"@shared": path.resolve(import.meta.dirname, "shared"), | |
"@assets": path.resolve(import.meta.dirname, "attached_assets"), | |
}, | |
}, | |
root: path.resolve(import.meta.dirname, "client"), | |
build: { | |
outDir: path.resolve(import.meta.dirname, "dist/public"), | |
emptyOutDir: true, | |
}, | |
server: { | |
fs: { | |
strict: true, | |
deny: ["**/.*"], | |
}, | |
}, | |
}); | |
import { defineConfig } from "vite"; | |
import react from "@vitejs/plugin-react"; | |
import path from "path"; | |
import { fileURLToPath } from "url"; | |
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal"; | |
export default defineConfig(async () => { | |
const rootDir = path.dirname(fileURLToPath(import.meta.url)); | |
const plugins = [react(), runtimeErrorOverlay()]; | |
if (process.env.NODE_ENV !== "production" && process.env.REPL_ID) { | |
const { cartographer } = await import("@replit/vite-plugin-cartographer"); | |
plugins.push(cartographer()); | |
} | |
return { | |
plugins, | |
resolve: { | |
alias: { | |
"@": path.resolve(rootDir, "client", "src"), | |
"@shared": path.resolve(rootDir, "shared"), | |
"@assets": path.resolve(rootDir, "attached_assets"), | |
}, | |
}, | |
root: path.resolve(rootDir, "client"), | |
build: { | |
outDir: path.resolve(rootDir, "dist/public"), | |
emptyOutDir: true, | |
}, | |
server: { | |
fs: { | |
strict: true, | |
deny: ["**/.*"], | |
}, | |
}, | |
}; | |
}); |
🤖 Prompt for AI Agents
In platforms/marketplace/vite.config.ts around lines 6 to 37, replace use of
nonstandard import.meta.dirname by deriving a directory via fileURLToPath(new
URL('.', import.meta.url)) (or equivalent) and update all path.resolve calls to
use that computed dirname; also convert the exported config to an async
defineConfig factory so you can await the conditional dynamic import of
@replit/vite-plugin-cartographer only when NODE_ENV !== "production" and REPL_ID
is set, then include the imported plugin in the plugins array or an empty array
otherwise, keeping the rest of the config (resolve.alias, root, build, server)
unchanged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
import * as React from "react" | ||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" | ||
|
||
import { cn } from "@/lib/utils" | ||
import { buttonVariants } from "@/components/ui/button" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mark as a Client Component.
This module wraps Radix UI (client-only). Add the "use client" directive to avoid Next.js Server Component import errors.
+ "use client"
+
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
import * as React from "react" | |
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" | |
import { cn } from "@/lib/utils" | |
import { buttonVariants } from "@/components/ui/button" | |
"use client" | |
import * as React from "react" | |
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" | |
import { cn } from "@/lib/utils" | |
import { buttonVariants } from "@/components/ui/button" |
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/alert-dialog.tsx lines 1-6,
this module imports Radix UI (a client-only library) but lacks the "use client"
directive; add a top-line "use client" directive as the first line of the file
so Next.js treats this as a Client Component and prevents Server Component
import errors, keeping the rest of the imports and code unchanged.
const AlertTitle = React.forwardRef< | ||
HTMLParagraphElement, | ||
React.HTMLAttributes<HTMLHeadingElement> | ||
>(({ className, ...props }, ref) => ( | ||
<h5 | ||
ref={ref} | ||
className={cn("mb-1 font-medium leading-none tracking-tight", className)} | ||
{...props} | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix ref/prop element type mismatches for AlertTitle/AlertDescription.
Refs and props types don’t match the rendered tags (h5 vs HTMLParagraphElement; div vs HTMLParagraphElement). This breaks type-safety and can confuse consumers.
Apply:
-const AlertTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLHeadingElement>
+const AlertTitle = React.forwardRef<
+ HTMLHeadingElement,
+ React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
-const AlertDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLParagraphElement>
+const AlertDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
Also applies to: 47-55
<span | ||
ref={ref} | ||
role="link" | ||
aria-disabled="true" | ||
aria-current="page" | ||
className={cn("font-normal text-foreground", className)} | ||
{...props} | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix a11y: current page should not have role="link" or aria-disabled.
The current page crumb isn’t interactive; keep aria-current="page" only.
<span
ref={ref}
- role="link"
- aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<span | |
ref={ref} | |
role="link" | |
aria-disabled="true" | |
aria-current="page" | |
className={cn("font-normal text-foreground", className)} | |
{...props} | |
/> | |
<span | |
ref={ref} | |
aria-current="page" | |
className={cn("font-normal text-foreground", className)} | |
{...props} | |
/> |
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/breadcrumb.tsx around lines
64–71, the current-page span incorrectly includes role="link" and
aria-disabled="true"; remove those two attributes and keep only
aria-current="page". Also ensure you don't accidentally forward interactive
attributes from the spread props by destructuring role and aria-disabled (e.g.
const { role, 'aria-disabled': ariaDisabled, ...rest } = props) and spreading
rest so the current crumb cannot receive a role or aria-disabled from callers.
const handleKeyDown = React.useCallback( | ||
(event: React.KeyboardEvent<HTMLDivElement>) => { | ||
if (event.key === "ArrowLeft") { | ||
event.preventDefault() | ||
scrollPrev() | ||
} else if (event.key === "ArrowRight") { | ||
event.preventDefault() | ||
scrollNext() | ||
} | ||
}, | ||
[scrollPrev, scrollNext] | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Don’t hijack Arrow keys inside inputs; support Up/Down for vertical carousels.
Currently Arrow keys are captured everywhere, breaking text inputs and textareas. Add a guard and map keys by orientation.
- const handleKeyDown = React.useCallback(
- (event: React.KeyboardEvent<HTMLDivElement>) => {
- if (event.key === "ArrowLeft") {
- event.preventDefault()
- scrollPrev()
- } else if (event.key === "ArrowRight") {
- event.preventDefault()
- scrollNext()
- }
- },
- [scrollPrev, scrollNext]
- )
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
+ const target = event.target as HTMLElement | null
+ if (target && target.closest('input, textarea, select, [contenteditable="true"]')) {
+ return
+ }
+ const key = event.key
+ if (orientation === "vertical") {
+ if (key === "ArrowUp") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (key === "ArrowDown") {
+ event.preventDefault()
+ scrollNext()
+ }
+ } else {
+ if (key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ }
+ },
+ [orientation, scrollPrev, scrollNext]
+ )
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const handleKeyDown = React.useCallback( | |
(event: React.KeyboardEvent<HTMLDivElement>) => { | |
if (event.key === "ArrowLeft") { | |
event.preventDefault() | |
scrollPrev() | |
} else if (event.key === "ArrowRight") { | |
event.preventDefault() | |
scrollNext() | |
} | |
}, | |
[scrollPrev, scrollNext] | |
) | |
const handleKeyDown = React.useCallback( | |
(event: React.KeyboardEvent<HTMLDivElement>) => { | |
const target = event.target as HTMLElement | null | |
if (target && target.closest('input, textarea, select, [contenteditable="true"]')) { | |
return | |
} | |
const key = event.key | |
if (orientation === "vertical") { | |
if (key === "ArrowUp") { | |
event.preventDefault() | |
scrollPrev() | |
} else if (key === "ArrowDown") { | |
event.preventDefault() | |
scrollNext() | |
} | |
} else { | |
if (key === "ArrowLeft") { | |
event.preventDefault() | |
scrollPrev() | |
} else if (key === "ArrowRight") { | |
event.preventDefault() | |
scrollNext() | |
} | |
} | |
}, | |
[orientation, scrollPrev, scrollNext] | |
) |
React.useEffect(() => { | ||
if (!api) { | ||
return | ||
} | ||
|
||
onSelect(api) | ||
api.on("reInit", onSelect) | ||
api.on("select", onSelect) | ||
|
||
return () => { | ||
api?.off("select", onSelect) | ||
} | ||
}, [api, onSelect]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Unsubscribe “reInit” listener to prevent duplicate handlers.
Missing cleanup for the reInit subscription can cause multiple onSelect calls after reinitialization.
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
- api?.off("select", onSelect)
+ api?.off("select", onSelect)
+ api?.off("reInit", onSelect)
}
}, [api, onSelect])
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
React.useEffect(() => { | |
if (!api) { | |
return | |
} | |
onSelect(api) | |
api.on("reInit", onSelect) | |
api.on("select", onSelect) | |
return () => { | |
api?.off("select", onSelect) | |
} | |
}, [api, onSelect]) | |
React.useEffect(() => { | |
if (!api) { | |
return | |
} | |
onSelect(api) | |
api.on("reInit", onSelect) | |
api.on("select", onSelect) | |
return () => { | |
api?.off("select", onSelect) | |
api?.off("reInit", onSelect) | |
} | |
}, [api, onSelect]) |
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/carousel.tsx around lines 107
to 119, the effect subscribes to both "reInit" and "select" events but only
removes the "select" listener on cleanup, which can leave dangling "reInit"
handlers and cause duplicate onSelect calls after reinitialization; update the
cleanup to unsubscribe both events (e.g., call api?.off("reInit", onSelect) and
api?.off("select", onSelect)) so all handlers added in the effect are removed
when the component unmounts or api changes, keeping the initial onSelect
invocation intact.
// Object storage routes | ||
app.get("/public-objects/:filePath(*)", async (req, res) => { | ||
const filePath = req.params.filePath; | ||
try { | ||
const file = await objectStorageService.searchPublicObject(filePath); | ||
if (!file) { | ||
return res.status(404).json({ error: "File not found" }); | ||
} | ||
objectStorageService.downloadObject(file, res); | ||
} catch (error) { | ||
console.error("Error searching for public object:", error); | ||
return res.status(500).json({ error: "Internal server error" }); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Validate public ACL in /public-objects/
Ensure only publicly visible files are served; otherwise a misconfigured path could expose private objects.
app.get("/public-objects/:filePath(*)", async (req, res) => {
const filePath = req.params.filePath;
try {
const file = await objectStorageService.searchPublicObject(filePath);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
- objectStorageService.downloadObject(file, res);
+ const canRead = await objectStorageService.canAccessObjectEntity({
+ objectFile: file,
+ });
+ if (!canRead) {
+ return res.status(404).json({ error: "File not found" });
+ }
+ objectStorageService.downloadObject(file, res);
} catch (error) {
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Object storage routes | |
app.get("/public-objects/:filePath(*)", async (req, res) => { | |
const filePath = req.params.filePath; | |
try { | |
const file = await objectStorageService.searchPublicObject(filePath); | |
if (!file) { | |
return res.status(404).json({ error: "File not found" }); | |
} | |
objectStorageService.downloadObject(file, res); | |
} catch (error) { | |
console.error("Error searching for public object:", error); | |
return res.status(500).json({ error: "Internal server error" }); | |
} | |
}); | |
// Object storage routes | |
app.get("/public-objects/:filePath(*)", async (req, res) => { | |
const filePath = req.params.filePath; | |
try { | |
const file = await objectStorageService.searchPublicObject(filePath); | |
if (!file) { | |
return res.status(404).json({ error: "File not found" }); | |
} | |
const canRead = await objectStorageService.canAccessObjectEntity({ | |
objectFile: file, | |
}); | |
if (!canRead) { | |
return res.status(404).json({ error: "File not found" }); | |
} | |
objectStorageService.downloadObject(file, res); | |
} catch (error) { | |
console.error("Error searching for public object:", error); | |
return res.status(500).json({ error: "Internal server error" }); | |
} | |
}); |
🤖 Prompt for AI Agents
In platforms/marketplace/server/routes.ts around lines 111 to 124, the route
serving /public-objects/:filePath(*) currently returns any found file without
verifying its public ACL; update the handler to check the file's public
visibility metadata (e.g., file.isPublic or file.acl/public flag) after
searchPublicObject returns and before calling downloadObject, return 403
Forbidden for non-public files, and log the access denial; also ensure the
filePath is normalized/sanitized if your storage SDK requires it to prevent path
traversal.
app.get("/objects/:objectPath(*)", async (req, res) => { | ||
try { | ||
const objectFile = await objectStorageService.getObjectEntityFile(req.path); | ||
objectStorageService.downloadObject(objectFile, res); | ||
} catch (error) { | ||
console.error("Error accessing object:", error); | ||
if (error instanceof ObjectNotFoundError) { | ||
return res.sendStatus(404); | ||
} | ||
return res.sendStatus(500); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Private object leakage: add ACL check before streaming /objects/
The route streams any located object with the server’s credentials, regardless of ACL; a non-owner can fetch private files if they know the path.
app.get("/objects/:objectPath(*)", async (req, res) => {
try {
const objectFile = await objectStorageService.getObjectEntityFile(req.path);
- objectStorageService.downloadObject(objectFile, res);
+ const canRead = await objectStorageService.canAccessObjectEntity({
+ userId: (req as any).user?.id,
+ objectFile,
+ });
+ if (!canRead) {
+ return res.sendStatus(403);
+ }
+ objectStorageService.downloadObject(objectFile, res);
} catch (error) {
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
app.get("/objects/:objectPath(*)", async (req, res) => { | |
try { | |
const objectFile = await objectStorageService.getObjectEntityFile(req.path); | |
objectStorageService.downloadObject(objectFile, res); | |
} catch (error) { | |
console.error("Error accessing object:", error); | |
if (error instanceof ObjectNotFoundError) { | |
return res.sendStatus(404); | |
} | |
return res.sendStatus(500); | |
} | |
}); | |
app.get("/objects/:objectPath(*)", async (req, res) => { | |
try { | |
const objectFile = await objectStorageService.getObjectEntityFile(req.path); | |
const canRead = await objectStorageService.canAccessObjectEntity({ | |
userId: (req as any).user?.id, | |
objectFile, | |
}); | |
if (!canRead) { | |
return res.sendStatus(403); | |
} | |
objectStorageService.downloadObject(objectFile, res); | |
} catch (error) { | |
console.error("Error accessing object:", error); | |
if (error instanceof ObjectNotFoundError) { | |
return res.sendStatus(404); | |
} | |
return res.sendStatus(500); | |
} | |
}); |
🤖 Prompt for AI Agents
In platforms/marketplace/server/routes.ts around lines 126 to 137, the route
currently streams any located object without checking ACLs; before calling
downloadObject, retrieve the object's ACL/metadata and enforce authorization for
req.user (or authenticated identity) — if the object is private and the
requester is not the owner or in an allowed list, return 403 and do not stream;
implement this by calling an authorization helper (e.g.,
objectStorageService.isAuthorizedToRead or object.metadata.isPublic) after
getObjectEntityFile and before downloadObject, and ensure unauthenticated
requests are treated as unauthorized for private objects.
export const users = pgTable("users", { | ||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), | ||
username: text("username").notNull().unique(), | ||
email: text("email").notNull().unique(), | ||
password: text("password").notNull(), | ||
isAdmin: boolean("is_admin").default(false).notNull(), | ||
createdAt: timestamp("created_at").defaultNow().notNull(), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use UUID column type and defaultRandom().
Aligns DB type with generator, removes reliance on pgcrypto casts, and improves type-safety.
-import { pgTable, text, varchar, integer, decimal, timestamp, boolean } from "drizzle-orm/pg-core";
+import { pgTable, text, varchar, integer, decimal, timestamp, boolean, uuid, pgEnum, uniqueIndex, check } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
- id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
+ id: uuid("id").primaryKey().defaultRandom(),
username: text("username").notNull().unique(),
email: text("email").notNull().unique(),
password: text("password").notNull(),
isAdmin: boolean("is_admin").default(false).notNull(),
- createdAt: timestamp("created_at").defaultNow().notNull(),
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
export const users = pgTable("users", { | |
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), | |
username: text("username").notNull().unique(), | |
email: text("email").notNull().unique(), | |
password: text("password").notNull(), | |
isAdmin: boolean("is_admin").default(false).notNull(), | |
createdAt: timestamp("created_at").defaultNow().notNull(), | |
}); | |
import { pgTable, text, varchar, integer, decimal, timestamp, boolean, uuid, pgEnum, uniqueIndex, check } from "drizzle-orm/pg-core"; | |
export const users = pgTable("users", { | |
id: uuid("id").primaryKey().defaultRandom(), | |
username: text("username").notNull().unique(), | |
email: text("email").notNull().unique(), | |
password: text("password").notNull(), | |
isAdmin: boolean("is_admin").default(false).notNull(), | |
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), | |
}); |
🤖 Prompt for AI Agents
In platforms/marketplace/shared/schema.ts around lines 7 to 14, the users.id
column uses varchar with a raw SQL default gen_random_uuid(); change it to use
the UUID column type and the library's defaultRandom() helper (e.g.,
uuid("id").primaryKey().defaultRandom()) so the DB type matches the generator
and you avoid pgcrypto casts; also update any imports if your schema library
requires a uuid column helper instead of varchar.
💡 Verification agent
🧩 Analysis chain
Verify pgcrypto extension or use defaultRandom().
If sticking with gen_random_uuid(), ensure pgcrypto is enabled; otherwise defaultRandom() avoids this dependency.
Run:
Also applies to: 16-30, 32-39
🏁 Script executed:
#!/usr/bin/env bash
# If migrations exist, check for CREATE EXTENSION pgcrypto; else confirm use of defaultRandom()
fd -a 'drizzle|migrations' platforms/marketplace -td -E node_modules | xargs -I{} rg -n "CREATE EXTENSION\s+pgcrypto|gen_random_uuid|defaultRandom" -S -n -C2 {} || true
Length of output: 166
🏁 Script executed:
#!/usr/bin/env bash
# Search entire repo for pgcrypto extension usage, defaultRandom, and gen_random_uuid
rg -n "CREATE EXTENSION\s+pgcrypto" -C2 .
rg -n "defaultRandom" -C2 .
rg -n "gen_random_uuid" -C2 .
Length of output: 1385
Ensure pgcrypto is enabled or switch to defaultRandom()
The sql\
gen_random_uuid()`defaults in
platforms/marketplace/shared/schema.ts(lines 8, 17, 33) require PostgreSQL’s
pgcryptoextension. Add a migration such as
CREATE EXTENSION IF NOT EXISTS pgcrypto;before creating these tables, or replace
default(sql`gen_random_uuid()`)with Drizzle ORM’s
defaultRandom()` to avoid the dependency.
🤖 Prompt for AI Agents
In platforms/marketplace/shared/schema.ts around lines 7 to 14 (and other id
defaults at ~lines 17 and 33), the use of default(sql`gen_random_uuid()`)
requires the pgcrypto extension; either add a migration that runs CREATE
EXTENSION IF NOT EXISTS pgcrypto; before creating these tables, or replace those
default(sql`gen_random_uuid()`) calls with Drizzle ORM’s defaultRandom() on the
id columns so UUIDs are generated without relying on pgcrypto; pick one approach
and consistently apply it to all id columns.
export const apps = pgTable("apps", { | ||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), | ||
name: text("name").notNull(), | ||
description: text("description").notNull(), | ||
fullDescription: text("full_description"), | ||
category: text("category").notNull(), | ||
link: text("link").notNull(), | ||
logoUrl: text("logo_url"), | ||
screenshots: text("screenshots").array(), | ||
status: text("status").default("active").notNull(), // active, pending, inactive | ||
averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0"), | ||
totalReviews: integer("total_reviews").default(0).notNull(), | ||
createdAt: timestamp("created_at").defaultNow().notNull(), | ||
updatedAt: timestamp("updated_at").defaultNow().notNull(), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
App table: tighten types, add enum for status, ensure updatedAt semantics.
- Use UUID for id.
- Enforce status via enum.
- Ensure averageRating is not null.
- Make timestamps timezone-aware and auto-update updatedAt in app-layer.
-export const apps = pgTable("apps", {
- id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
+export const appStatusEnum = pgEnum("app_status", ["active", "pending", "inactive"]);
+
+export const apps = pgTable("apps", {
+ id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description").notNull(),
fullDescription: text("full_description"),
category: text("category").notNull(),
link: text("link").notNull(),
logoUrl: text("logo_url"),
screenshots: text("screenshots").array(),
- status: text("status").default("active").notNull(), // active, pending, inactive
- averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0"),
+ status: appStatusEnum("status").default("active").notNull(),
+ averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0").notNull(),
totalReviews: integer("total_reviews").default(0).notNull(),
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
}, (t) => ({
+ // optional: unique app names or links
+ // nameUniqueIdx: uniqueIndex("apps_name_unique_idx").on(t.name),
+ // linkUniqueIdx: uniqueIndex("apps_link_unique_idx").on(t.link),
}));
Note: For Postgres, updatedAt won’t auto-update on UPDATE; ensure the ORM layer sets it in mutations (e.g., updatedAt = new Date()).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
export const apps = pgTable("apps", { | |
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), | |
name: text("name").notNull(), | |
description: text("description").notNull(), | |
fullDescription: text("full_description"), | |
category: text("category").notNull(), | |
link: text("link").notNull(), | |
logoUrl: text("logo_url"), | |
screenshots: text("screenshots").array(), | |
status: text("status").default("active").notNull(), // active, pending, inactive | |
averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0"), | |
totalReviews: integer("total_reviews").default(0).notNull(), | |
createdAt: timestamp("created_at").defaultNow().notNull(), | |
updatedAt: timestamp("updated_at").defaultNow().notNull(), | |
}); | |
export const appStatusEnum = pgEnum("app_status", ["active", "pending", "inactive"]); | |
export const apps = pgTable("apps", { | |
id: uuid("id").primaryKey().defaultRandom(), | |
name: text("name").notNull(), | |
description: text("description").notNull(), | |
fullDescription: text("full_description"), | |
category: text("category").notNull(), | |
link: text("link").notNull(), | |
logoUrl: text("logo_url"), | |
screenshots: text("screenshots").array(), | |
status: appStatusEnum("status").default("active").notNull(), | |
averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0").notNull(), | |
totalReviews: integer("total_reviews").default(0).notNull(), | |
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), | |
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), | |
}, (t) => ({ | |
// optional: unique app names or links | |
// nameUniqueIdx: uniqueIndex("apps_name_unique_idx").on(t.name), | |
// linkUniqueIdx: uniqueIndex("apps_link_unique_idx").on(t.link), | |
})); |
🤖 Prompt for AI Agents
In platforms/marketplace/shared/schema.ts around lines 16 to 30, tighten the
apps table types: change id to a UUID column (use the DB/ORM uuid type and
default gen_random_uuid()), replace the freeform status text with a Postgres
enum (define and use a pgEnum like "app_status" with values
"active","pending","inactive"), mark averageRating as not null and keep a
numeric/decimal default of 0, change createdAt/updatedAt to timezone-aware
timestamps (timestamptz) and keep defaultNow() for createdAt, and ensure
updatedAt is set to defaultNow() but rely on the application/ORM layer to assign
updatedAt = new Date() on updates (document or implement update middleware/hooks
accordingly).
export const reviews = pgTable("reviews", { | ||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), | ||
appId: varchar("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }), | ||
username: text("username").notNull(), | ||
rating: integer("rating").notNull(), // 1-5 | ||
comment: text("comment"), | ||
createdAt: timestamp("created_at").defaultNow().notNull(), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Review table: use UUID FK, add rating CHECK and single-review per user constraint.
Improves integrity and prevents out-of-range ratings or duplicate reviews.
-export const reviews = pgTable("reviews", {
- id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
- appId: varchar("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }),
+export const reviews = pgTable("reviews", {
+ id: uuid("id").primaryKey().defaultRandom(),
+ appId: uuid("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }),
username: text("username").notNull(),
rating: integer("rating").notNull(), // 1-5
comment: text("comment"),
- createdAt: timestamp("created_at").defaultNow().notNull(),
-});
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
+}, (t) => ({
+ ratingRangeCk: check("reviews_rating_range_ck", sql`${t.rating} >= 1 AND ${t.rating} <= 5`),
+ oneReviewPerUserIdx: uniqueIndex("reviews_app_username_unique_idx").on(t.appId, t.username),
+}));
Optional: prefer userId referencing users.id instead of denormalized username for referential integrity.
Also applies to: 46-51
🤖 Prompt for AI Agents
In platforms/marketplace/shared/schema.ts around lines 32-39 (and similarly
46-51), the reviews table needs stronger integrity: change appId to a UUID
foreign key referencing apps.id (use the UUID/uuidv4 column type consistent with
apps), add a CHECK constraint on rating to enforce BETWEEN 1 AND 5, and add a
uniqueness constraint to prevent duplicate reviews per user (either UNIQUE on
(app_id, username) if keeping username, or preferably replace username with
userId referencing users.id and add UNIQUE(app_id, user_id)). Update both
occurrences accordingly and preserve createdAt.defaultNow().notNull().
Description of change
adds marketplace to metastate
Issue Number
n/a
Type of change
How the change has been tested
n/a
Change checklist
Summary by CodeRabbit